package cn.gyn.gynrpc.core.provider;

import cn.gyn.gynrpc.core.annotation.GynProvider;
import cn.gyn.gynrpc.core.api.RegistryCenter;
import cn.gyn.gynrpc.core.config.AppConfigProperties;
import cn.gyn.gynrpc.core.config.ProviderProperties;
import cn.gyn.gynrpc.core.meta.InstanceMeta;
import cn.gyn.gynrpc.core.meta.ProviderMeta;
import cn.gyn.gynrpc.core.meta.ServiceMeta;
import cn.gyn.gynrpc.core.util.MethodUtils;
import jakarta.annotation.PostConstruct;
import jakarta.annotation.PreDestroy;
import lombok.Data;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;

import java.lang.reflect.Method;
import java.net.InetAddress;
import java.util.Arrays;
import java.util.Map;

/**
 * 服务提供者的启动类
 *
 * ApplicationContextAware：
 * 如果一个Bean实现了ApplicationContextAware接口，Spring容器在将其实例化后将会调用setApplicationContext方法，将应用程序上下文的引用传递给该Bean
 *
 * 这里需要将ProviderBootstrap声明为Bean:虽然@Data注解可以自动生成Getter和Setter方法，但它并不能直接作为Spring Bean进行注入。
 * 在Spring框架中，要将一个类声明为Bean，通常需要使用@Component、@Service、@Repository或@Controller等注解来标记该类，以便Spring容器能够识别并管理它。
 * Q : 为什么不直接在类上声明Bean，而选择用@Configuration单独加配置类进行声明？不是有很多@Component、@Service、@Repository或@Controller等可以声明Bean的注解么？
 * A: @Component、@Service、@Repository或@Controller可以隐式声明类并注入，而通过@Bean注解来显式地声明Bean，可以使得整个配置更加清晰可控
 */
@Data
@Slf4j
public class ProviderBootstrap implements ApplicationContextAware {
    //去拿所有的bean，这里applicationContext的命名需要与ApplicationContext一致
    private ApplicationContext applicationContext;
    private RegistryCenter rc;
    private String port;
    private AppConfigProperties appProperties;
    private ProviderProperties providerProperties;
    //这是一个通过PostConstruct获取到的，有接口全限定名和对应类的桩子（这个桩子是服务提供者端的？），
    //这个桩子用于RPC请求时，通过RpcRequest请求里的service接口全限定名，获取到提供者端的具体实现类的bean。
    //private Map<String, Object> skeleton = new HashMap<>();
    //这里改为多值map:LinkedMultiValueMap（语法糖， 实际是一个String对应一个List<ProviderMeta>）,用一个方法的全限定名，对应到实际的方法对象。
    private MultiValueMap<String, ProviderMeta> skeleton = new LinkedMultiValueMap<>();
    //用于RC里注册provider的实例
    private InstanceMeta instance;

    public ProviderBootstrap(String port, AppConfigProperties appProperties,
                             ProviderProperties providerProperties) {
        this.port = port;
        this.appProperties = appProperties;
        this.providerProperties = providerProperties;
    }


    //用于RC里注册provider的实例里的端口号获取，这里写了这么多yaml，怎么知道是获取哪个项目的配置文件的port的？
    //在多模块项目中，使用@Value注解获取的值是根据配置文件的加载顺序来确定的,优先加载主模块的，然后再根据依赖关系获取到被依赖的模块的配置
    /*@Value("${server.port}")
    private String port;

    @Value("${app.id}")
    private String app;

    @Value("${app.namespace}")
    private String namespace;

    @Value("${app.env}")
    private String env;

    @Value("#{${app.metas}}") //#{}是一个Spel:Spring Expression Language spring表达式语言
    Map<String, String> metas;*/

    //问题：在spring里，所有的bean被初始化但没完成创建，或着刚完成创建，在所有调用开始之前想去加一些操作有哪几种方法？
    //在spring容器启动以后，业务方法执行之前，把加了注解的服务拿出并存起来
    //A:比如：beanpostprocessor\重写init\initializingBean
    //老师构造porvider时用的方法是：@PostConstruct  其本质就是init method,但在消费者端没办法使用
    //所有的bean和对象被创建了，但还没有实现和初始化各种属性、赋值装配，此时所有bean的定义、bean的空桩子已经被创建了
    @SneakyThrows
    @PostConstruct
    //PostConstruct完全相当于init-method，所以他的销毁方法就是destory method,destory method对应的注解是PreDestory
    //PreDestory将会用到注册中心，当服务下线，对注册的内容进行销毁
    public void init() {
        //拿到所有打了这个自定义注解的bean
        Map<String, Object> providers = applicationContext.getBeansWithAnnotation(GynProvider.class);
        rc = applicationContext.getBean(RegistryCenter.class);
        //这里运行后看看打印的是什么？
        providers.keySet().forEach(System.out::println);
        //skeleton.putAll(providers);//这里的x是名字，不是接口，而skeleton里的x应该是实现接口的全限定名,即UserServiceImpl的全限定名，所以不能这么写putAll
        //获取GynProvider注解的类给skeleton赋值
        providers.values().forEach(this::genInterface);
        /*上面可以直接写为providers.values().forEach(
                x -> genInterface(x)
        );*/
    }

    //这里引入@SneakyThrows注解，处理检查异常为非检查异常
    @SneakyThrows
    public void start() {
        //获取本地主机的IP地址
        String ip = InetAddress.getLocalHost().getHostAddress();
        //这里用下划线代替冒号，因为冒号有特殊含义？//把灰度的配置放进实例的参数里
        instance = InstanceMeta.http(ip, Integer.valueOf(port)).addParams(providerProperties.getMetas());
        rc.start();
        //skeleton里的key就是server的集合，所以，先通过keySet()方法获取到skeleton的键的Set集合，然后使用forEach方法对Set集合中的每个键进行遍历。在forEach方法中，我们可以编写自己的逻辑来处理每个键。
        //this::方法名 表示引用当前对象的 "registerService" 方法。用于将方法作为参数传递给其他方法、在函数式编程中使用、或者简化代码等情况下。通过这个函数引用，可以传递或调用该方法，而不需要显式地访问对象实例。
        //key作为方法registerService的入参
        skeleton.keySet().forEach(this::registerService);

        //这里有个惊天大bug：spring init-method还没有完成，就已经把服务注册到zk了，可能导致调用失败
        //解决方案，将init-method和start方法区分开，让start从config里在spring上下文加载完成后再注册。
    }

    @PreDestroy
    public void stop() {
        log.info(" ===> provider unregisterService stop");
        skeleton.keySet().forEach(this::unregisterService);
        rc.stop();
    }

    /**
     * 调用RC的register方法，注册Provider到注册中心
     * @param service
     */
    private void registerService(String service) {
        //这里用builder模式，除了service参数以外的属性，可以通过demo-provider的配置文件配+通过命令行启动后springboot的参数重载，需要的时候给重载掉。
        ServiceMeta serviceMeta = ServiceMeta.builder()
                .name(service)
                .app(appProperties.getId())
                .env(appProperties.getEnv())
                .namespace(appProperties.getNamespace())
                .build();
        rc.register(serviceMeta, instance);
    }

    /**
     * 调用RC的unregister方法，从注册中心注销Provider
     * @param service
     */
    private void unregisterService(String service) {
        ServiceMeta serviceMeta = ServiceMeta.builder()
                .name(service)
                .app(appProperties.getId())
                .env(appProperties.getEnv())
                .namespace(appProperties.getNamespace())
                .build();
        rc.unRegister(serviceMeta, instance);
    }

    /**
     *
     * @param impl 具体实现类
     */
    private void genInterface(Object impl) {
        //Class<?> itface = impl.getClass().getInterfaces()[0];//先拿到x的类，我们简化的假设只有一个接口，返回接口Class（实际也可以是多个接口都抽出来给skeleton Map）

        //一个接口两个实现类，怎么处理是SPI的东西， 不是API的东西
        //如果一个接口有多个实现类可以循环处理，比如public class UserServiceImpl implements UserService, OrderService {...}
        Arrays.stream(impl.getClass().getInterfaces()).forEach(
                service -> {
                    /*//skeleton.put(service.getCanonicalName(), impl);//往skeleton里添加接口的全限定名[kəˈnɔnikəl]规范的，和x类本身
                    //skeleton结构变了，给skeleton重新赋值
                    Method[] methods = service.getMethods();
                    for (Method m : methods) {
                        //这里获取方法名，比较判断object的基础方法，(点进Object,从idea左边三个方块的structure里看)，进行null返回过滤，避免这些方法被远程调用到
                        //为了避免服务真的有同名的重载方法，可以用带参数的方法的签名代替方法名来判断
                        //if (MethodUtils.checkLocalMethod(m.getName())) {
                        if (MethodUtils.checkLocalMethod(m)) {
                            continue;
                        }
                        createProviderSkeleton(service, impl, m);
                    }*/
                    Arrays.stream(service.getMethods())
                            .filter(method -> !MethodUtils.checkLocalMethod(method))
                            .forEach(method -> {
                                createProviderSkeleton(service, impl, method);
                            });
                }
        );
    }

    private void createProviderSkeleton(Class<?> service, Object impl, Method method) {
        ProviderMeta providerMeta = ProviderMeta.builder()
                .method(method)
                .methodSign(MethodUtils.methodSign(method))
                .serviceImpl(impl)
                .build();
        log.info("create a providerMeta" + providerMeta);
        skeleton.add(service.getCanonicalName(), providerMeta);
    }
}
